package org.example.server.goods.service.category.impl;

import cn.hutool.core.text.CharSequenceUtil;
import cn.hutool.json.JSONUtil;
import jakarta.annotation.Resource;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.example.api.goods.category.request.CategoryCreateRequest;
import org.example.api.goods.category.request.CategorySearchWithLevelRequest;
import org.example.api.goods.category.request.CategoryUpdateRequest;
import org.example.frw.api.base.intf.ID;
import org.example.frw.api.base.intf.Pageable;
import org.example.frw.api.base.intf.impl.IDPojo;
import org.example.frw.mq.rocket.DelayDoubleDelete;
import org.example.frw.verify.annotations.Verify;
import org.example.server.goods.mapstruct.category.CategoryConverter;
import org.example.server.goods.repo.CategoryRepo;
import org.example.server.goods.repo.doo.CategorySummaryDo;
import org.example.server.goods.service.category.CategoryService;
import org.example.server.goods.service.category.bo.CategorySummaryBo;
import org.example.server.goods.service.category.verify.CreateCategoryVerifyImpl;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.concurrent.TimeUnit;

@Service
@Slf4j
public class CategoryServiceImpl implements CategoryService {
  @Resource
  private CategoryRepo categoryRepo;
  @Resource
  private CategoryConverter categoryConverter;
  @Resource
  private RedissonClient redissonClient;
  @Resource
  private RedisTemplate<String, String> redisTemplate;
  @Resource
  private DelayDoubleDelete delayDoubleDelete;

  @Value("${minio.minio-endpoint:http://127.0.0.1:8000}")
  private String minioEndpoint;



  /**
   * 创建一个新的分类
   * @param category category
   * @return 主键
   */
  @Override
  @Verify(verifalClass = CreateCategoryVerifyImpl.class)
  public ID createCategory(CategoryCreateRequest category) {
    return categoryRepo.insert(category);
  }

  /**分页查询分类
   * @param page page
   * @param search search
   * @return 分类列表
   */
  @Override
  @Verify(verifalClass = CreateCategoryVerifyImpl.class)
  public List<CategorySummaryBo> pageCategories(Pageable page, CategorySearchWithLevelRequest search) {
    List<CategorySummaryDo> categorySummaryDos = categoryRepo.pageCategories(page, search);
    categorySummaryDos.forEach(categorySummaryDo -> {
      categorySummaryDo.setCategoryImageUrl(minioEndpoint + categorySummaryDo.getCategoryImageUrl());
    });
    return categoryConverter.toCategorySummaryFromDo(categorySummaryDos);
  }

  /**删除某个分类
   * @param id id
   */
  @Override
  public void hide(ID id) {
    categoryRepo.hide(id);
  }


  @Override
  public Long categoriesCount(CategorySearchWithLevelRequest categorySearchRequest) {
    return categoryRepo.categoriesCount(categorySearchRequest);
  }

  /**
   *
   * @param id id
   * @return
   */
  @Override
  public CategorySummaryBo findOne(Long id) {
    String categoryCacheKey = String.format("category:id:%s", id);
    return getFromCacheOrDatabase(categoryCacheKey, id);
  }

  private CategorySummaryBo getFromCacheOrDatabase(String categoryCacheKey, Long id) {
    // 续租5分钟
    String categoryJson = redisTemplate.opsForValue().getAndExpire(categoryCacheKey,5, TimeUnit.MINUTES);
    if (CharSequenceUtil.isNotBlank(categoryJson)) {
      return JSONUtil.toBean(categoryJson, CategorySummaryBo.class);
    }
    return loadFromDatabase(categoryCacheKey, id);
  }

  /**
   * 如果多线程同时查询，大部分情况下会从缓存走。如果数据库发生变化，key会消失（因为删了），就要去查询数据库
   * 但是，不能让多线程同时查询数据库，因为查询数据库还得写缓存（性能不好），不如加锁，只让一个线程去查数据库，其他线程等待
   * 直到当前线程维护完毕缓存，其他线程被依次被唤醒（只有一个能拿到锁），会重新查询一遍缓存，这次有了就返回，
   * 如果没有（概率极低，只可能是在此期间数据库又发生了更新），这个线程再去维护缓存，其他线程等待
   * @param categoryCacheKey categoryCacheKey
   * @param id id
   * @return CategorySummaryBo
   */
  private CategorySummaryBo loadFromDatabase(String categoryCacheKey, Long id) {
    RLock lock = redissonClient.getLock(String.format("lock:%s", categoryCacheKey));
    boolean locked = false;
    try {
      // 尝试获取锁，并设置超时时间，避免死锁
      locked = lock.tryLock(5, TimeUnit.SECONDS);
      if (!locked) {
        // 如果无法获取锁，直接返回null，避免过多的数据库访问
        return null;
      }
      // 再次检查缓存，可能在等待锁的时候，其他线程已经加载了数据
      String categoryJson = redisTemplate.opsForValue().get(categoryCacheKey);
      if (CharSequenceUtil.isNotBlank(categoryJson)) {
        return JSONUtil.toBean(categoryJson, CategorySummaryBo.class);
      }
      CategorySummaryBo summaryFromDo = categoryConverter.toCategorySummaryFromDo(categoryRepo.findOne(new IDPojo(id)));
      redisTemplate.opsForValue().set(categoryCacheKey, JSONUtil.toJsonStr(summaryFromDo), 5, TimeUnit.MINUTES);
      return summaryFromDo;
    } catch (InterruptedException e) {
      // 恢复中断状态
      Thread.currentThread().interrupt();
      // 记录日志
      log.error("{}",e.getMessage(),e);
    } finally {
      if (lock != null && locked) {
        lock.unlock();
      }
    }
   return null;

  }

  @Override
  @SneakyThrows
  public void updateCategory(ID id, CategoryUpdateRequest updateRequest) {
    String categoryCacheKey = "category:id:%s".formatted(id.id());
    delayDoubleDelete.delayDoubleDelete(categoryCacheKey, () -> {
      categoryRepo.updateCategory(id, updateRequest);
    });
  }

}
